﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Abp;
using Abp.Dependency;
using Abp.Domain.Entities;
using Abp.Domain.Entities.Caching;
using Abp.Domain.Repositories;
using Abp.Domain.Uow;
using Abp.EntityFrameworkCore.Repositories;
using Abp.Events.Bus.Entities;
using Abp.Events.Bus.Handlers;
using Abp.ObjectMapping;
using Abp.Runtime.Caching;

namespace Hicap.AuthorizationServer.Core.Caches.Base
{
    public class EntityCacheBase<TEntity, TCacheItem> :
        EntityCacheBase<TEntity, TCacheItem, int>,
            IEntityCache<TCacheItem>
            where TEntity : class, IEntity<int>
    {
        public EntityCacheBase(
            ICacheManager cacheManager,
            IRepository<TEntity, int> repository,
            string cacheName = null)
            : base(
                cacheManager,
                repository,
                cacheName)
        {
        }
    }

    public class EntityCacheBase<TEntity, TCacheItem, TPrimaryKey> :
        IEventHandler<EntityChangedEventData<TEntity>>,
        IEntityCache<TCacheItem, TPrimaryKey>
        where TEntity : class, IEntity<TPrimaryKey>
    {
        private IUnitOfWorkManager _unitOfWorkManager = IocManager.Instance.Resolve<IUnitOfWorkManager>();
        public TCacheItem this[TPrimaryKey id] => Get(id);

        public string CacheName { get; private set; }

        public ITypedCache<TPrimaryKey, TCacheItem> InternalCache => CacheManager.GetCache<TPrimaryKey, TCacheItem>(CacheName);

        public IObjectMapper ObjectMapper { get; set; }

        protected ICacheManager CacheManager { get; private set; }

        protected IRepository<TEntity, TPrimaryKey> Repository { get; private set; }

        public EntityCacheBase(
            ICacheManager cacheManager,
            IRepository<TEntity, TPrimaryKey> repository,
            string cacheName = null)
        {
            Repository = repository;
            CacheManager = cacheManager;
            CacheName = cacheName ?? GenerateDefaultCacheName();
            ObjectMapper = NullObjectMapper.Instance;
        }

        public virtual List<TCacheItem> GetAll(object propertySelector,
            params Expression<Func<TEntity, object>>[] propertySelectors)
        {
            ITypedCache<int, List<TCacheItem>> extendCache = CacheManager.GetCache<int, List<TCacheItem>>(CacheName + ".FullData");
            return extendCache.Get(-1, () => GetAllDataFromDataSource(propertySelectors));
        }

        private List<TCacheItem> GetAllDataFromDataSource(params Expression<Func<TEntity, object>>[] propertySelectors)
        {
            using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant))
            {
                var query = Repository.GetAllIncluding(propertySelectors);
                var dbList = query.ToList();
                return ObjectMapper.Map<List<TCacheItem>>(dbList);
            }
        }

        public virtual TCacheItem Get(TPrimaryKey id)
        {
            return InternalCache.Get(id, () => GetCacheItemFromDataSource(id));
        }

        public virtual Task<TCacheItem> GetAsync(TPrimaryKey id)
        {
            return InternalCache.GetAsync(id, () => GetCacheItemFromDataSourceAsync(id));
        }

        public virtual void HandleEvent(EntityChangedEventData<TEntity> eventData)
        {
            if (eventData is EntityDeletedEventData<TEntity>)
            {
                InternalCache.Remove(eventData.Entity.Id);
            }
            else
            {
                var cache = MapToCacheItem(eventData.Entity);
                InternalCache.Set(eventData.Entity.Id, cache);
            }
        }

        protected virtual TCacheItem GetCacheItemFromDataSource(TPrimaryKey id)
        {
            return MapToCacheItem(GetEntityFromDataSource(id));
        }

        protected virtual async Task<TCacheItem> GetCacheItemFromDataSourceAsync(TPrimaryKey id)
        {
            return MapToCacheItem(await GetEntityFromDataSourceAsync(id));
        }

        protected virtual TEntity GetEntityFromDataSource(TPrimaryKey id)
        {
            //            return Repository.FirstOrDefault(id);
            var entity = Repository.GetAll().FirstOrDefault(CreateEqualityExpressionForId(id));
            if (entity != null) Repository.DetachFromDbContext(entity);
            return entity;
        }

        protected virtual Task<TEntity> GetEntityFromDataSourceAsync(TPrimaryKey id)
        {
            return Task.FromResult(GetEntityFromDataSource(id));
            //            return Repository.FirstOrDefaultAsync(id);
        }

        protected virtual TCacheItem MapToCacheItem(TEntity entity)
        {
            if (ObjectMapper is NullObjectMapper)
            {
                throw new AbpException(
                    string.Format(
                        "MapToCacheItem method should be overrided or IObjectMapper should be implemented in order to map {0} to {1}",
                        typeof(TEntity),
                        typeof(TCacheItem)
                        )
                    );
            }

            return ObjectMapper.Map<TCacheItem>(entity);
        }

        protected virtual string GenerateDefaultCacheName()
        {
            return GetType().FullName;
        }

        public override string ToString()
        {
            return string.Format("EntityCache {0}", CacheName);
        }

        protected static Expression<Func<TEntity, bool>> CreateEqualityExpressionForId(TPrimaryKey id)
        {
            var lambdaParam = Expression.Parameter(typeof(TEntity));

            var lambdaBody = Expression.Equal(
                Expression.PropertyOrField(lambdaParam, "Id"),
                Expression.Constant(id, typeof(TPrimaryKey))
            );

            return Expression.Lambda<Func<TEntity, bool>>(lambdaBody, lambdaParam);
        }
    }
}